/**
 * DLA/Oscilloscope Project Software
 * 
 * By Kevin Cuzner <kevincuzner@yahoo.com>
 * 
 * This file is part of the DLA/Oscilloscope Project Software.
 *
 * The DLA/Oscilloscope Project Software is free software: you can 
 * redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * The DLA/Oscilloscope Project Software is distributed in the hope
 * that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with The DLA/Oscilloscope Project Software.
 * If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;

namespace DLAOscilloscopegtk
{
	/**
	 * Analog channel based on a set of analog samples
	 */
	public class SampleAnalogChannel : IAnalogChannel
	{
		protected List<Sample> samples;
		protected ulong timeBase; //the current time base for this channel
		protected ulong nativeTimeBase; //frequency in hz this was originally sampled at
		protected double zero; //offset of the data 0 value before scaling
		protected double scaling; //number of raw data increments to 1 volt
		protected string name; //the name of this channel
		
		/**
		 * Constructs an analog channel based on some samples
		 * @param data A sample group containg the set of points to create this channel from
		 * @param zero Offset of the 0 value in the raw data before scaling
		 * @param scaling Number of raw data units to 1 volt
		 */
		public SampleAnalogChannel (SampleGroup data, double zero, double scaling)
		{
			//check arguments
			if (data.Type != SampleType.ANALOG)
				throw new ArgumentException ("SampleGroup data must contain analog samples to build an analog channel");
			this.samples = data.Samples;
			this.nativeTimeBase = data.Frequency;
			
			this.zero = zero;
			this.scaling = scaling;
			this.name = data.Name;
		}
		
		/**
		 * Returns the zeroed and scaled value for the point at x
		 * @param x number of the sample using 1/TimeBase units
		 * @returns zeroed and scaled value
		 */
		public double GetPointAt (int x)
		{
			int sCount = this.samples.Count - 1; //the index of the final sample
			
			if (x < 0) {
				//this is a problem...they are trying to access before the samples started. Use our first point
				return ((double)this.samples [0].Value + this.zero) / this.scaling;
			}
			
			if (timeBase != nativeTimeBase) {
				//adjust and interpolate the sample to match the requested timebase
				double tbRatio = (double)nativeTimeBase / (double)timeBase;
				if (tbRatio < 1) {
					//they are looking for more detail that this data has to offer. Interpolate a new value.
					/** To Do: Learn more calculus and implement a smooth interpolation rather than this point splitting method **/
					int lIndex = (int)Math.Floor ((double)x * tbRatio); //the index of the point closest to the left of the one we are to create
					int rIndex = (int)Math.Ceiling ((double)x * tbRatio); //the index of the point closest to the right of the one we are to create
					if (lIndex > this.samples.Count - 1 || rIndex > sCount) {
						//it is trying to index something outside the bounds of our dataset. Use the final datapoint
						return ((double)this.samples [sCount].Value + this.zero) / this.scaling;
					} else if (lIndex == rIndex) {
						//it happens to land right on an already existing point
						return ((double)this.samples [lIndex].Value + this.zero) / this.scaling;
					} else {
						//interpolate between lValue and rValue
						double lValue = ((double)this.samples [lIndex].Value + this.zero) / this.scaling;
						double rValue = ((double)this.samples [rIndex].Value + this.zero) / this.scaling;
						double ratio = (double)x * tbRatio - Math.Floor ((double)x * tbRatio);
						return lValue * (1 - ratio) + rValue * ratio;
					}
				} else {
					//they are looking for less detail. Average some point values
					int lIndex = (int)Math.Floor (((double)x - 0.5) * tbRatio); //leftmost point to use in the averaging
					int rIndex = (int)Math.Ceiling (((double)x + 0.5) * tbRatio); //rightmost point to use in the averaging
					double avg = 0;
					int c = 0;
					if (lIndex < 0) {
						/** Todo: Use a weighted average rather than just truncating points **/
						rIndex += lIndex; //chop off as many samples on the right as don't exist on the left
						lIndex = 0;
					}
					if (rIndex > sCount) {
						/** Todo: Use a weighted average rather than just truncating points **/
						lIndex += rIndex - sCount; //chop off as many samples on the left as don't exist on the right
						rIndex = sCount;
					}
					
					if (rIndex < lIndex) {
						//somehow, we removed too many points. This does not return a value. Return 0
						return 0.0;
					}
					
					//average the points we have selected
					for (int i = lIndex; i <= rIndex; i++) {
						avg += ((double)this.samples [i].Value + this.zero) / this.scaling;
						c++;
					}
					avg /= c; //create the average
					return avg; //and return it
				}
			} else {
				//zero and scale the value and return it
				if (x > sCount) {
					//they are trying to index something outisde the bounds of our dataset. Use the final value
					return ((double)this.samples [sCount].Value + this.zero) / this.scaling;
				}
				return ((double)this.samples [x].Value + this.zero) / this.scaling;
			}
		}
		
		/**
		 * Returns the zeroed and scaled value for the point at x. Arguments are ignored in this channel
		 * @param x number of the sample using 1/TimeBase units
		 * @param arguments ignored in this class
		 * @returns zered and scaled value
		 */
		public double GetPointAt (int x, params double[] arguments)
		{
			return GetPointAt (x);
		}
		
		public ulong NativeTimeBase {
			get {
				return nativeTimeBase;
			}
		}
		
		public ulong TimeBase {
			get {
				return timeBase;
			}
			set {
				timeBase = value;
			}
		}
		
		public double Zero {
			get {
				return zero;
			}
			set {
				zero = value;
			}
		}
		
		public double Scaling {
			get {
				return scaling;
			}
			set {
				scaling = value;
			}
		}
	}
}

